/* Copyright (C) 2004 - 2008  Versant Inc.  http://www.db4o.com

This file is part of the sharpen open source java to c# translator.

sharpen is free software; you can redistribute it and/or modify it under
the terms of version 2 of the GNU General Public License as published
by the Free Software Foundation and as clarified by db4objects' GPL 
interpretation policy, available at
http://www.db4o.com/about/company/legalpolicies/gplinterpretation/
Alternatively you can write to db4objects, Inc., 1900 S Norfolk Street,
Suite 350, San Mateo, CA 94403, USA.

sharpen is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
for more details.

You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
59 Temple Place - Suite 330, Boston, MA  02111-1307, USA. */

package sharpen.core;

import org.eclipse.jdt.core.dom.*;
import sharpen.core.csharp.ast.*;

import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

public class CSAnonymousClassBuilder extends AbstractNestedClassBuilder {

    private AnonymousClassDeclaration _node;

    private CSClass _type;

    private CSConstructor _constructor;

    private Set<IVariableBinding> _capturedVariables = new LinkedHashSet<IVariableBinding>();

    private CSharpBuilder _parent;

    private Set<VariableDeclarationFragment> _fieldInitializers = new LinkedHashSet<VariableDeclarationFragment>();

    public CSAnonymousClassBuilder(CSharpBuilder builder, AnonymousClassDeclaration node) {
        super(builder);
        _parent = builder;
        _node = node;
        run();
    }

    public CSClass type() {
        return _type;
    }

    public Set<IVariableBinding> capturedVariables() {
        return _capturedVariables;
    }

    public CSExpression createConstructorInvocation() {
        CSConstructorInvocationExpression invocation = new CSConstructorInvocationExpression(new CSReferenceExpression(_type.name()));
        if (isEnclosingReferenceRequired()) {
            invocation.addArgument(new CSThisExpression());
        }
        addCapturedVariables(invocation);
        addBaseConstructorArguments(invocation);
        return invocation;
    }

    private void addCapturedVariables(CSConstructorInvocationExpression invocation) {
        for (IVariableBinding variable : _capturedVariables) {
            invocation.addArgument(new CSReferenceExpression(identifier(variable.getName())));
        }
    }

    private void addBaseConstructorArguments(CSConstructorInvocationExpression invocation) {
        List arguments = classInstanceCreationArguments();
        if (arguments.isEmpty()) {
            return;
        }

        final ITypeBinding[] ctorParameterTypes = classInstanceCreation().resolveConstructorBinding().getParameterTypes();

        if (null == _constructor.chainedConstructorInvocation())
            _constructor.chainedConstructorInvocation(new CSConstructorInvocationExpression(new CSBaseExpression()));

        for (int i = 0; i < ctorParameterTypes.length; ++i) {
            ITypeBinding parameterType = ctorParameterTypes[i];
            Expression argument = (Expression) arguments.get(i);

            String parameterName = "baseArg" + (i + 1);
            _constructor.addParameter(parameterName, mappedTypeReference(parameterType));
            _constructor.chainedConstructorInvocation().addArgument(new CSReferenceExpression(parameterName));

            invocation.addArgument(_parent.mapExpression(argument));
        }
    }

    private List classInstanceCreationArguments() {
        return classInstanceCreation().arguments();
    }

    private ClassInstanceCreation classInstanceCreation() {
        return ((ClassInstanceCreation) _node.getParent());
    }

    public void run() {
        captureExternalLocalVariables();
        setUpAnonymousType();
        setUpConstructor();
        processAnonymousBody();
        int capturedVariableCount = flushCapturedVariables();
        flushFieldInitializers();
        flushInstanceInitializers(_type, capturedVariableCount);
    }

    private void flushFieldInitializers() {

        for (VariableDeclarationFragment field : _fieldInitializers) {
            addToConstructor(createFieldAssignment(fieldName(field), mapExpression(field.getInitializer())));
        }
    }

    @Override
    protected CSExpression mapFieldInitializer(VariableDeclarationFragment fragment) {
        if (fragment.getInitializer() != null) {
            _fieldInitializers.add(fragment);
        }
        return null;
    }

    private void processAnonymousBody() {
        CSTypeDeclaration saved = _currentType;
        _currentType = _type;
        visit(_node.bodyDeclarations());
        _currentType = saved;
    }

    public boolean visit(AnonymousClassDeclaration node) {
        CSAnonymousClassBuilder builder = new CSAnonymousClassBuilder(this, node);
        if (builder.isEnclosingReferenceRequired()) {
            requireEnclosingReference();
        }
        captureNeededVariables(builder);
        pushExpression(builder.createConstructorInvocation());
        _currentType.addMember(builder.type());
        return false;
    }

    private void captureNeededVariables(CSAnonymousClassBuilder builder) {

        IMethodBinding currentMethod = currentMethodDeclarationBinding();
        for (IVariableBinding variable : builder.capturedVariables()) {
            IMethodBinding method = variable.getDeclaringMethod();
            if (method != currentMethod) {
                _capturedVariables.add(variable);
            }
        }
    }

    private IMethodBinding currentMethodDeclarationBinding() {
        return _currentBodyDeclaration instanceof MethodDeclaration
                ? ((MethodDeclaration) _currentBodyDeclaration).resolveBinding()
                : null;
    }

    private void addFieldParameter(String name, CSTypeReferenceExpression type) {
        addFieldParameter(CSharpCode.newPrivateReadonlyField(name, type));
    }

    private void addFieldParameter(CSField field) {
        _type.addMember(field);

        String parameterName = field.name();
        _constructor.addParameter(parameterName, field.type());
        addToConstructor(createFieldAssignment(field.name(), parameterName));
    }

    private void addToConstructor(final CSExpression expression) {
        _constructor.body().addStatement(expression);
    }

    private String anonymousBaseTypeName() {
        return mappedTypeName(anonymousBaseType());
    }

    public ITypeBinding anonymousBaseType() {
        ITypeBinding binding = nestedTypeBinding();
        return binding.getInterfaces().length > 0
                ? binding.getInterfaces()[0]
                : binding.getSuperclass();
    }

    @Override
    protected ITypeBinding nestedTypeBinding() {
        return _node.resolveBinding();
    }

    private String anonymousInnerClassName() {
        return "_" + simpleName(anonymousBaseTypeName()) + "_" + lineNumber(_node);
    }

    private String simpleName(String typeName) {
        final int index = typeName.lastIndexOf('.');
        if (index < 0) return typeName;
        return typeName.substring(index + 1);
    }

    private void setUpAnonymousType() {
        _type = classForAnonymousType();
    }

    private CSClass classForAnonymousType() {
        CSClass type = new CSClass(anonymousInnerClassName(), CSClassModifier.Sealed);
        type.visibility(CSVisibility.Private);
        ITypeBinding bt = anonymousBaseType();
        CSTypeReference tref = new CSTypeReference(anonymousBaseTypeName());
        type.addBaseType(tref);
        for (ITypeBinding arg : bt.getTypeArguments()) {
            tref.addTypeArgument(mappedTypeReference(arg));
        }
        ITypeBinding tt = anonymousBaseType();
        for (ITypeBinding tp : tt.getTypeParameters())
            type.addTypeParameter(new CSTypeParameter(identifier(tp.getName())));
        return type;
    }

    private void setUpConstructor() {
        _constructor = new CSConstructor();
        _constructor.visibility(CSVisibility.Public);
        _type.addMember(_constructor);
    }

    private int flushCapturedVariables() {
        int capturedVariableCount = 0;
        if (isEnclosingReferenceRequired()) {
            capturedVariableCount++;
            CSField ef = createEnclosingField();
            addFieldParameter(ef);
            ITypeBinding bt = anonymousBaseType();
            if (bt != null && isNonStaticNestedType(bt)) {
                if (null == _constructor.chainedConstructorInvocation())
                    _constructor.chainedConstructorInvocation(new CSConstructorInvocationExpression(new CSBaseExpression()));
                _constructor.chainedConstructorInvocation().addArgument(new CSReferenceExpression(ef.name()));
            }
        }

        for (IVariableBinding variable : _capturedVariables) {
            capturedVariableCount++;
            addFieldParameter(identifier(variable.getName()), mappedTypeReference(variable.getType()));
        }

        return capturedVariableCount;
    }

    private void captureExternalLocalVariables() {
        _node.accept(new ASTVisitor() {

            IMethodBinding _currentMethodBinding;

            public boolean visit(MethodDeclaration node) {
                IMethodBinding saved = _currentMethodBinding;
                _currentMethodBinding = node.resolveBinding();
                node.getBody().accept(this);
                _currentMethodBinding = saved;
                return false;
            }

            public boolean visit(AnonymousClassDeclaration node) {
                return node == _node;
            }

            public boolean visit(SimpleName node) {
                IBinding binding = node.resolveBinding();
                if (isExternalLocal(binding)) {
                    _capturedVariables.add((IVariableBinding) binding);
                }
                return false;
            }

            boolean isExternalLocal(IBinding binding) {
                if (binding instanceof IVariableBinding) {
                    IVariableBinding variable = (IVariableBinding) binding;
                    if (!variable.isField()) {
                        return variable.getDeclaringMethod() != _currentMethodBinding;
                    }
                }
                return false;
            }
        });
    }

}
